// 版权所有2011 Go作者。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

package test

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"cmd/go/internal/base"
	"cmd/go/internal/cfg"
	"cmd/go/internal/cmdflag"
	"cmd/go/internal/work"
)

// 开始：生成开始运行。/genflags.go

// 围棋测试的旗子处理部分很大，而且会分散注意力。
// 无法使用（*flag.FlagSet）.Parse，因为
// 我们的命令行是为我们准备的，有些是为测试二进制文件准备的，还有
// 有些是两者都适用的。

func init() {
	work.AddBuildFlags(CmdTest, work.OmitVFlag)

	cf := CmdTest.Flag
	cf.BoolVar(&testC, "c", false, "")
	cf.BoolVar(&cfg.BuildI, "i", false, "")
	cf.StringVar(&testO, "o", "", "")

	cf.BoolVar(&testCover, "cover", false, "")
	cf.Var(coverFlag{(*coverModeFlag)(&testCoverMode)}, "covermode", "")
	cf.Var(coverFlag{commaListFlag{&testCoverPaths}}, "coverpkg", "")

	cf.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
	cf.BoolVar(&testJSON, "json", false, "")
	cf.Var(&testVet, "vet", "")

	// 要转发到测试二进制文件的寄存器标志。我们保留变量用于
	// 其中一些是为了让cmd/go知道如何处理测试输出，或者知道
	// 以支持使用标志的方式构建测试。

	cf.StringVar(&testBench, "bench", "", "")
	cf.Bool("benchmem", false, "")
	cf.String("benchtime", "", "")
	cf.StringVar(&testBlockProfile, "blockprofile", "", "")
	cf.String("blockprofilerate", "", "")
	cf.Int("count", 0, "")
	cf.Var(coverFlag{stringFlag{&testCoverProfile}}, "coverprofile", "")
	cf.String("cpu", "", "")
	cf.StringVar(&testCPUProfile, "cpuprofile", "", "")
	cf.Bool("failfast", false, "")
	cf.StringVar(&testList, "list", "", "")
	cf.StringVar(&testMemProfile, "memprofile", "", "")
	cf.String("memprofilerate", "", "")
	cf.StringVar(&testMutexProfile, "mutexprofile", "", "")
	cf.String("mutexprofilefraction", "", "")
	cf.Var(&testOutputDir, "outputdir", "")
	cf.Int("parallel", 0, "")
	cf.String("run", "", "")
	cf.Bool("short", false, "")
	cf.DurationVar(&testTimeout, "timeout", 10*time.Minute, "")
	cf.StringVar(&testTrace, "trace", "", "")
	cf.BoolVar(&testV, "v", false, "")
	cf.Var(&testShuffle, "shuffle", "")

	for name := range passFlagToTest {
		cf.Var(cf.Lookup(name).Value, "test."+name, "")
	}
}

// coverFlag是一个标志。该值也表示-cover。
type coverFlag struct{ v flag.Value }

func (f coverFlag) String() string { return f.v.String() }

func (f coverFlag) Set(value string) error {
	if err := f.v.Set(value); err != nil {
		return err
	}
	testCover = true
	return nil
}

type coverModeFlag string

func (f *coverModeFlag) String() string { return string(*f) }
func (f *coverModeFlag) Set(value string) error {
	switch value {
	case "", "set", "count", "atomic":
		*f = coverModeFlag(value)
		return nil
	default:
		return errors.New(`valid modes are "set", "count", or "atomic"`)
	}
}

// commaListFlag是一个标志。值表示以逗号分隔的列表。
type commaListFlag struct{ vals *[]string }

func (f commaListFlag) String() string { return strings.Join(*f.vals, ",") }

func (f commaListFlag) Set(value string) error {
	if value == "" {
		*f.vals = nil
	} else {
		*f.vals = strings.Split(value, ",")
	}
	return nil
}

// stringFlag是一个标志。表示单个字符串的值。
type stringFlag struct{ val *string }

func (f stringFlag) String() string { return *f.val }
func (f stringFlag) Set(value string) error {
	*f.val = value
	return nil
}

// outputdirFlag实现-outputdir标志。
// 它将空值解释为“go”命令的工作目录。
type outputdirFlag struct {
	abs string
}

func (f *outputdirFlag) String() string {
	return f.abs
}
func (f *outputdirFlag) Set(value string) (err error) {
	if value == "" {
		f.abs = ""
	} else {
		f.abs, err = filepath.Abs(value)
	}
	return err
}
func (f *outputdirFlag) getAbs() string {
	if f.abs == "" {
		return base.Cwd()
	}
	return f.abs
}

// vetFlag实现-vet标志的特殊解析逻辑：
// 以逗号分隔的列表，具有可分辨值“off”和
// 跟踪是否显式设置的布尔值。
type vetFlag struct {
	explicit bool
	off      bool
	flags    []string // 在“go test”期间自动调用时传递给vet
}

func (f *vetFlag) String() string {
	if f.off {
		return "off"
	}

	var buf strings.Builder
	for i, f := range f.flags {
		if i > 0 {
			buf.WriteByte(',')
		}
		buf.WriteString(f)
	}
	return buf.String()
}

func (f *vetFlag) Set(value string) error {
	if value == "" {
		*f = vetFlag{flags: defaultVetFlags}
		return nil
	}

	if value == "off" {
		*f = vetFlag{
			explicit: true,
			off:      true,
		}
		return nil
	}

	if strings.Contains(value, "=") {
		return fmt.Errorf("-vet argument cannot contain equal signs")
	}
	if strings.Contains(value, " ") {
		return fmt.Errorf("-vet argument is comma-separated list, cannot contain spaces")
	}
	*f = vetFlag{explicit: true}
	for _, arg := range strings.Split(value, ",") {
		if arg == "" {
			return fmt.Errorf("-vet argument contains empty list element")
		}
		f.flags = append(f.flags, "-"+arg)
	}
	return nil
}

type shuffleFlag struct {
	on   bool
	seed *int64
}

func (f *shuffleFlag) String() string {
	if !f.on {
		return "off"
	}
	if f.seed == nil {
		return "on"
	}
	return fmt.Sprintf("%d", *f.seed)
}

func (f *shuffleFlag) Set(value string) error {
	if value == "off" {
		*f = shuffleFlag{on: false}
		return nil
	}

	if value == "on" {
		*f = shuffleFlag{on: true}
		return nil
	}

	seed, err := strconv.ParseInt(value, 10, 64)
	if err != nil {
		return fmt.Errorf(`-shuffle argument must be "on", "off", or an int64: %v`, err)
	}

	*f = shuffleFlag{on: true, seed: &seed}
	return nil
}

// testFlags处理命令行，获取-x和-c，重写已知标志
// 将“test”放在前面，并读取测试二进制文件的命令行。
// 不幸的是，我们需要做自己的标记处理，因为要进行测试
// 获取一些标志，但除此之外，它的命令行只是一个等待的地方
// test的参数。
// 我们允许在包名称列表之前和之后使用已知标志，
// 兼得
// go test fmt-fmt测试的自定义标志
// 参加数学考试
func testFlags(args []string) (packageNames, passToTest []string) {
	base.SetFromGOFLAGS(&CmdTest.Flag)
	addFromGOFLAGS := map[string]bool{}
	CmdTest.Flag.Visit(func(f *flag.Flag) {
		if short := strings.TrimPrefix(f.Name, "test."); passFlagToTest[short] {
			addFromGOFLAGS[f.Name] = true
		}
	})

	// firstUnknownFlag帮助我们在不知道标志“go”时报告错误
	// test'与-i或-c一起使用。
	firstUnknownFlag := ""

	explicitArgs := make([]string, 0, len(args))
	inPkgList := false
	afterFlagWithoutValue := false
	for len(args) > 0 {
		f, remainingArgs, err := cmdflag.ParseOne(&CmdTest.Flag, args)

		wasAfterFlagWithoutValue := afterFlagWithoutValue
		afterFlagWithoutValue = false // 暂时的

		if errors.Is(err, flag.ErrHelp) {
			exitWithUsage()
		}

		if errors.Is(err, cmdflag.ErrFlagTerminator) {
			// “go list”允许在包参数之前或之后命名
			// 终结者，但“去测试”历史上只允许他们
			// 之前保留该行为并处理所有剩余的参数-
			// 包括终结者本身作为测试的参数。
			explicitArgs = append(explicitArgs, args...)
			break
		}

		if nf := (cmdflag.NonFlagError{}); errors.As(err, &nf) {
			if !inPkgList && packageNames != nil {
				// 我们之前已经看到了包列表，而这个参数不是
				// 一个标志，所以它-以及它后面的所有内容-必须是
				// 测试二进制文件的前置标志或文字参数。
				if wasAfterFlagWithoutValue {
					// 这个参数在语法上可以是一个标志值，所以
					// 乐观地假设是这样，并继续寻找go命令
					// 后面是旗帜。
					// None
					// （如果我们错了，我们至少会与历史数据保持一致
					// 行为请参阅https:
					explicitArgs = append(explicitArgs, nf.RawArg)
					args = remainingArgs
					continue
				} else {
					// 此参数在语法上不能是标志值，因此必须是
					// 位置参数，以及其后的所有内容。
					explicitArgs = append(explicitArgs, args...)
					break
				}
			}

			inPkgList = true
			packageNames = append(packageNames, nf.RawArg)
			args = remainingArgs // 使用包名。
			continue
		}

		if inPkgList {
			// 这个参数在语法上是一个标志，所以如果我们在包中
			// 我们不再是了。
			inPkgList = false
		}

		if nd := (cmdflag.FlagNotDefinedError{}); errors.As(err, &nd) {
			// 这是一面我们不知道的旗帜。我们必须假设我们看到的任何参数
			// 在这之后可能是标志参数，而不是包名，所以
			// packageNames non nil表示包装清单已完成。
			// None
			// （事实上，我们只需严格假设如果旗帜不是
			// 形式-x=值，但使其更精确将是一个突破
			// 命令行API中的更改。）
			if packageNames == nil {
				packageNames = []string{}
			}

			if nd.RawArg == "-args" || nd.RawArg == "--args" {
				// -args或--args表示后面的所有内容
				// 应该通过测试。
				explicitArgs = append(explicitArgs, remainingArgs...)
				break
			}

			if firstUnknownFlag == "" {
				firstUnknownFlag = nd.RawArg
			}

			explicitArgs = append(explicitArgs, nd.RawArg)
			args = remainingArgs
			if !nd.HasValue {
				afterFlagWithoutValue = true
			}
			continue
		}

		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			exitWithUsage()
		}

		if short := strings.TrimPrefix(f.Name, "test."); passFlagToTest[short] {
			explicitArgs = append(explicitArgs, fmt.Sprintf("-test.%s=%v", short, f.Value))

			// 此标志已被显式重写，因此不要转发其隐式
			// 来自GOFLAGS的值。
			delete(addFromGOFLAGS, short)
			delete(addFromGOFLAGS, "test."+short)
		}

		args = remainingArgs
	}
	if firstUnknownFlag != "" && (testC || cfg.BuildI) {
		buildFlag := "-c"
		if !testC {
			buildFlag = "-i"
		}
		fmt.Fprintf(os.Stderr, "go test: unknown flag %s cannot be used with %s\n", firstUnknownFlag, buildFlag)
		exitWithUsage()
	}

	var injectedFlags []string
	if testJSON {
		// 如果转换为JSON，我们需要完整的输出，以便将其传输到
		// test2json。
		injectedFlags = append(injectedFlags, "-test.v=true")
		delete(addFromGOFLAGS, "v")
		delete(addFromGOFLAGS, "test.v")
	}

	// 在显式命令行参数之前插入来自GOFLAGS的标志。
	// （它们必须出现在标志终止符或第一个非标志参数之前。）
	// 还要确定是否已经设置了默认值不合适的标志。
	var timeoutSet, outputDirSet bool
	CmdTest.Flag.Visit(func(f *flag.Flag) {
		short := strings.TrimPrefix(f.Name, "test.")
		if addFromGOFLAGS[f.Name] {
			injectedFlags = append(injectedFlags, fmt.Sprintf("-test.%s=%v", short, f.Value))
		}
		switch short {
		case "timeout":
			timeoutSet = true
		case "outputdir":
			outputDirSet = true
		}
	})

	// “go test”有默认超时，但测试二进制文件本身没有。
	// 如果没有明确设置（并转发）超时，请添加默认值
	// 命令行超时。
	if testTimeout > 0 && !timeoutSet {
		injectedFlags = append(injectedFlags, fmt.Sprintf("-test.timeout=%v", testTimeout))
	}

	// 类似地，测试二进制文件将-test.outputdir默认为其自己的工作状态
	// 目录，但“go test”默认为“go”的工作目录
	// 命令如果由于某些其他标志而需要，请显式设置它
	// 请求输出。
	if testProfile() != "" && !outputDirSet {
		injectedFlags = append(injectedFlags, "-test.outputdir="+testOutputDir.getAbs())
	}

	// 如果用户显式传递-help或-h，则显示输出
	// 以显示帮助输出
	// 即使测试将成功退出。
	// 这个循环是不完美的：它会对一个案例做错误的事情
	// like-args-test.outputdir-help。这种情况可能很少见，
	// 把这个弄错不会有太大的伤害。
helpLoop:
	for _, arg := range explicitArgs {
		switch arg {
		case "--":
			break helpLoop
		case "-h", "-help", "--help":
			testHelp = true
			break helpLoop
		}
	}

	// 确保-race和-covermode兼容。
	if testCoverMode == "" {
		testCoverMode = "set"
		if cfg.BuildRace {
			// 设置-race时，默认覆盖模式为原子模式。
			testCoverMode = "atomic"
		}
	}
	if cfg.BuildRace && testCoverMode != "atomic" {
		base.Fatalf(`-covermode must be "atomic", not %q, when -race is enabled`, testCoverMode)
	}

	// 将任何未解析的参数（在--args之后）转发到测试二进制文件。
	return packageNames, append(injectedFlags, explicitArgs...)
}

func exitWithUsage() {
	fmt.Fprintf(os.Stderr, "usage: %s\n", CmdTest.UsageLine)
	fmt.Fprintf(os.Stderr, "Run 'go help %s' and 'go help %s' for details.\n", CmdTest.LongName(), HelpTestflag.LongName())

	base.SetExitStatus(2)
	base.Exit()
}
